/**
 * 
 */
package net.bingosoft.common.upload;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.bingosoft.common.ObjectRegistry;
import net.bingosoft.common.config.Config;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.MultipartStream.MalformedStreamException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 文件上传统一管理控制类
 * 
 * @author elvis
 */
public class UploadManager {
    
    private static final Logger log = LoggerFactory.getLogger(UploadManager.class);
    
    private static UploadConfig config = null;
    /**
     * 上传文件Id到文件路径的缓存清理时间间隔,以秒为单位
     */
    private static Long UPLOAD_CACHE_REFRESH_TIME = Config.getLong("UPLOAD_CACHE_REFRESH_TIME", 60L);
    
    private static long idcounter = 1;
    
    /** 上传文件缓存map，每1分钟检查一次 */
    private static final TimeoutMap map = new TimeoutMap(Executors.newScheduledThreadPool(1),UPLOAD_CACHE_REFRESH_TIME*1000);
    
    public static UploadConfig getConfig() {
        if(null == config){
            config = new UploadConfig();
        }
        return config;
    }

    /**
     * 设置全局配置信息类
     */
    public static void setConfig(UploadConfig config) {
        UploadManager.config = config;
    }
    
    /**
     * 自动查找spring上下文中是否定义了配置信息类，beanId = "UploadConfig"
     */
    public static boolean configBySpring(ServletContext context){
        String beanId = "UploadConfig";
        if(ObjectRegistry.containsBean(beanId)){
        	UploadConfig config = ObjectRegistry.getBean(beanId,UploadConfig.class);
        	UploadManager.setConfig(config);
        	return true;
        }
        return false;
    }
    
    /**
     * 上传文件
     */
    public static UploadResult upload(HttpServletRequest request,HttpServletResponse response){
        return upload(request,response,null);
    }

    /**
     * 上传文件
     */
    @SuppressWarnings("unchecked")
    public static UploadResult upload(HttpServletRequest request,HttpServletResponse response,UploadConfig config){
        if(ServletFileUpload.isMultipartContent(request)){
            List<String> ids = new ArrayList<String>();
            
            try {
                if(null == config){
                    log.debug("current request config not defined,using global");
                    config = new UploadConfig();
                    config.copyFrom(getConfig());
                }
                
                log.debug("found multipart form,start pasing upload form...");
                
                ServletFileUpload upload = new ServletFileUpload();
                upload.setHeaderEncoding(config.getEncoding());
                FileItemIterator iterator = upload.getItemIterator(request);
                
                log.debug("upload form finish pasing,start processing it...");
                
                while(iterator.hasNext()){
                    FileItemStream item  = (FileItemStream)iterator.next();
                    if(!item.isFormField()){
                        if(log.isDebugEnabled()){
                            log.debug("found upload field '{}' : '{}'",item.getFieldName(),item.getName());
                        }
                        
                        UploadFile file = new UploadFile();
                        file.setId(String.valueOf(nextId()));
                        file.setClientPath(item.getName());  
                        file.setUploadFolder(config.getUploadFolder());
                        
                        //generate server path and file name
                        File diskFile = null;
                        boolean generated = false;
                        while(!generated){
                            diskFile = null;
                            
                            file.setServerFolder(config.getUploadPolicy().generateServerFolder(config, file, request));
                            file.setServerName(config.getUploadPolicy().generateServerName(config, file, request));
                            
                            log.debug("generated serverFolder '{}',serverName '{}'",file.getServerFolder(),file.getServerName());
                            
                            diskFile = new File(file.getServerPath());
                            
                            if(!diskFile.exists()){
                                generated = true;
                            }else{
                                log.info("file is exist of generated server folder and name,continue to generate new one");
                            }
                        }
                        
                        InputStream stream   = null;
                        FileOutputStream out = null;                        
                        try{
                            File folder = new File(file.getUploadFolder() + file.getServerFolder());
                            if(!folder.exists()){
                                if(log.isDebugEnabled()){
                                    log.debug("folder '{}' is not exists,create it",file.getUploadFolder() + file.getServerFolder());
                                }
                                folder.mkdirs();
                            }
                            
                            stream = item.openStream();
                            out    = new FileOutputStream(diskFile);
                            
                            log.debug("start saving upload file to path {}....",file.getServerPath());  
                            
                            int total   = 0;
                            int read    = 0;
                            int cache   = 0;
                            byte[] buff = new byte[512]; //allocate 512 byte
                            while((read = stream.read(buff)) > 0){
                                total += read;
                                cache += read;
                                //check max file size exceed
                                if(config.getMaxFileSize() > 0 && total > config.getMaxFileSize()){
                                    log.debug("file exceeed max file size {},cleanup and return",config.getMaxFileSize());
                                    //cleanup
                                    cleanup(request,stream, out, diskFile);
                                    return UploadResult.failed(Messages.getString("max_file_size") + getSizeOfKB(config.getMaxFileSize()) + "kb");
                                }else{
                                    out.write(buff, 0, read);
                                    if(cache > 2048 * 1024){
                                        //flush every 1M
                                        log.debug("2M buff full,flush it to disk");
                                        out.flush();
                                        cache = 0;
                                    }
                                }
                            }
                            
                            if(!config.isAllowEmpty() && total <= 0){
                                //cleanup
                                cleanup(request,stream, out, diskFile);
                                return UploadResult.failed(Messages.getString("not_allow_empty"));
                            }
                            
                            if(cache > 0){
                                out.flush();
                            }
                        }finally{
                            close(stream, out, diskFile);
                        }

                        log.debug("file wrote to disk successfully,now store the info into cache map for future used!"); 
                        
                        //store to the cache
                        map.put(file.getId(), file, config.getTimeout() * 1000);
                        
                        ids.add(file.getId());
                    }else{
                        config(item,config);
                    }
                }
                if(ids.size() == 1){
                    return UploadResult.success(ids.get(0)); 
                }else{
                    String[] fileid = new String[ids.size()];
                    for(int i=0;i<ids.size();i++){
                        fileid[i] = ids.get(i);
                    }
                    return UploadResult.success(fileid);                        
                }                
            } catch (MalformedStreamException e){
                log.warn("malformed stream exception,may be aborted by user : {}",e.getMessage());
                for(String id : ids){
                    UploadManager.deleteUploadFile(id);
                }
                return UploadResult.failed(Messages.getString("upload_aborted"));
            } catch (Exception e) {
                log.error("file upload error", e); //$NON-NLS-1$
                return UploadResult.failed(Messages.getString("upload_exception") +  ":" + e.getMessage()); //$NON-NLS-1$
            }
        }else{
            log.warn("current request has no a multipart form,ignore it,just return failed");
        }
        return UploadResult.failed(Messages.getString("no_upload_files")); //$NON-NLS-1$
    }
    
    /**
     * 根据文件ID拿到上传文件
     * @param fileid 文件的唯一id
     */
    public static UploadFile getUploadFile(String fileid){
        return (UploadFile)map.get(fileid);
    }
    
    /**
     * 从cache中清除上传文件信息
     */
    public static void removeUploadFile(String fileid){
        map.remove(fileid);
    }
    
    /**
     * 删除上传的文件并从cache中清除
     */
    public static void deleteUploadFile(String fileid){
        UploadFile file = getUploadFile(fileid);
        if(null != file){
            log.debug("found upload file {} to be deleted",file.getServerPath());
            File diskFile = new File(file.getServerPath());
            if(diskFile.exists()){
                diskFile.delete();
                log.debug("delete the file in disk successfully");
            }else{
                log.debug("file is not exists in disk,ignore to delete it");
            }
            removeUploadFile(fileid);
        }else{
            log.debug("file {} not found in cached map,may be remove by other process or timeout",fileid);
        }
    }
    
    @SuppressWarnings("unchecked")
    private static void config(FileItemStream item,UploadConfig config) throws IOException{
        if(item.isFormField()){
            String name = item.getFieldName();
            if(UploadConfig.isConfigField(name)){
                InputStream stream = item.openStream();
                String value = Streams.asString(stream,config.getEncoding());
                log.debug("found a config field {},override to the default config",name);
                config.config(name, value);
            }
        }
    }
    
    private static void cleanup(HttpServletRequest request, InputStream stream,FileOutputStream out,File file){
        try {
            try{
                if(null != out){
                    out.close();
                }
            }finally{
                try{
                    file.delete();
                }finally{
                    try{
                        request.getInputStream().close();
                    }finally{
                        if(null != stream){
                            stream.close();
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.warn("excepiton cleaning up upload stream:{}",e.getMessage());
        }
    }
    
    private static void close(InputStream stream,FileOutputStream out,File file){
        try {
            try{
                if(null != out){
                    out.close();
                }
            }finally{
                if(null != stream){
                    stream.close();
                }
            }
        } catch (Exception e) {
            log.warn("excepiton closing upload streams : {}",e.getMessage());
        }
    }
    
    private static String getSizeOfKB(long size){
        String value = String.valueOf((float)size / (float)1024);
        int dotIndex = value.indexOf(".");
        if(dotIndex < 0){
            return value;
        }else{
            return value.substring(0,value.length() > dotIndex + 2 ? dotIndex + 3 : value.length());
        }
    }
    
    private static synchronized long nextId(){
        long id = idcounter++;
        if(id == Long.MAX_VALUE){
            idcounter = 1;
        }
        return id;
    }
}
